Validation Admission Policy

개요

승인 제어을 확장하는 방법 중 가장 많이 쓰였던 방식은 웹훅 방식이었다.
그러나 이를 쿠버 자체적으로 내장하는 방향으로 조금씩 발전이 이뤄지고 있는데, 그 중 첫번째로 구현된 것이 바로 검증 승인 정책이다.
1.30버전 기준으로는 stable 상태이고, 아직 한참 멀었지만 Mutating Admission Policy도 언젠가 GA가 되지 않을까 한다.
문서에서도 검증 승인 웹훅의 선언적, 내장된 대체제라고 표현한다.
이것도 결국 하나의 오브젝트인데, 특징은 CEL 표현식을 이용한다는 것이다.

구조

(위 화살표들은 실제 요청이 검증되기 위해 거치는 흐름을 나타낸 것에 가깝다.)
정책(ValidatingAdmissionPolicy)과 정책 바인딩(ValidatingAdmissionPolicyBinding) 두 가지 기본 리소스가 있다.

ValidatingAdmissionPolicy

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: "demo-policy.example.com"
spec:
  matchConstraints:
    resourceRules:
    - apiGroups:   ["apps"]
      apiVersions: ["v1"]
      operations:  ["CREATE", "UPDATE"]
      resources:   ["deployments"]
  validations:
    - expression: "object.spec.replicas <= 5"
  failurePolicy: Fail

정책의 기본적인 로직이 적히는 오브젝트이다.
예시를 보는 게 이해가 빠른데, 일단 디플로이먼트를 만들거나 업데이트하는 요청에 대해 이 정책이 적용된다.
그리고 검증 규칙은 CEL 표현식으로, 레플리카가 5개 이하여야 참이 된다.
만약 거짓이라면 failurePolicy에 걸리는데, 지금의 경우는 정책에서 실패를 반환한다.

matchConstraints

위처럼 그냥 어떤 리소스를 검증하려는 건지 제약을 지정하는 필드이다.
당연히 와일드카드도 사용할 수 있다.
굳이 꼭 이걸로 씅이 안 차는 당신을 위해... 추가적으로 [[#matchConditions]]를 지정하는 것도 가능하다..

validations

  validations:
    - expression: "object.spec.replicas <= 5"
      reason: Forbidden
	  message: "params missing but required to bind to this policy"
	  messageExpression: "'object.spec.replicas must be no greater than ' + string(params.maxReplicas)"

검증을 수행하는 필드로, 리스트로 넣으면 차례대로 검증을 수행한다.
하나라도 거짓을 반환하면 일단 거짓이 반환되는데, 이에 대한 결정은 아래 [[#failurePolicy]]에서 결정된다.

reason 필드를 넣어서 거짓이 출력될 값을 지정할 수 있다.
이 값은 그대로 사용자에게는 HTTP 에러 코드로 나타나게 된다.
가능한 값은 다음의 것들이 있는데 세팅하지 않으면 사용자에게는 StatusReasonInvalid라고 뜨게 된다.

message 필드를 통해 출력 메시지를 지정할 수 있는데, 굳이 또 CEL까지 써주고 싶으면 messageExpressions를 쓰자.
두 필드를 같이 쓸 수 있는데, 우선순위는 표현식 쪽이긴 하다.
근데 간혹 댕청한 관리자가 messageExpressions에서 에러가 나오게 식을 썼다면, message 필드가 대신 출력된다.

CEL 변수

여기에서 CEL을 사용하게 되니, 구체적으로 어떤 변수를 사용할 수 있는 지도 보자.
검증 정책이 적용될 때 사용되는 각 변수는 다음과 같다.

오브젝트라는 변수들은 전부 다음의 값은 무조건 가지고 있다.

사실 당연한 거긴 한데, 아무튼 참고하자.

failurePolicy

이 정책의 검증에 대한 실패 정책을 지정한다.
무슨 말이냐, 어떤 요청이 들어오면 이 정책의 CEL 표현식에서 참이나 거짓이 반환될 것이다.
이때 거짓이 반환된 경우 이 정책이 해당 요청에 대해 어떤 결정을 내릴지를 지정한다는 것이다.
여기에는 Ignore, Fail 두 가지 값이 가능하다.
말 그대로 Ignore는 표현식에서 거짓이 나오게 되더라도 무시라는 결정을 내릴 것이다.
다만 여기에서 Faile을 한다고 무조건 해당 요청이 실패한다는 것은 아니다.
아래 [[#ValidatingAdmissionPolicyBinding]]를 보면 알겠지만, Fail이란 결정이 일어나도, 어떻게 동작할지를 또 세부 지정할 수 있다.

Ignore 할 거면 정책을 왜 쓰니?

라는 질문을 한 당신 다시 한번 생각해보십시오
Ignore은 정책 설정에 있어 유연성을 더해준다.
일단 테스트 환경일 때 무작정 모든 요청이 fail 뜨도록 하는 것은 디버깅이나 운영을 어렵게 만든다.
그렇기에 처음에는 Ignore로 해뒀다가 본격적으로 운영할 때부터 Fail로 둔다던가 하는 전략이 유효할 것이다.
정책을 쓸 때만 정책을 만들었다 아닐 때 삭제하는 식은 힘들 수 있잖냐..
그리고 정책은 여러 개 작성할 수 있으니, 어느 하나가 잠시 Ignore여도 다른 정책에서 평가될 때 Fail이 뜨게 하는 식으로도 운용이 가능하다.

paramKind

  paramKind:
    apiVersion: rules.example.com/v1
    kind: ReplicaLimit
  validations:
    - expression: "object.spec.replicas <= params.maxReplicas"
      reason: Invalid
    - expression: "params != null"
	  message: "params missing but required to bind to this policy"

정책에 대한 특정 부분들을 정책으로부터 분리시키는 방법이 있다.
가령 다른 정책에서도 많이 쓰이고 통일시켜야 하는 값은 일찌감치 분리시켜 재사용하는 것이 유용할 텐데, 이때 파라미터를 쓴다.
파라미터의 스키마는 spec.paramKind에 넣어주면 된다.

위 예시는 일단 마음대로 ReplicaLimit이란 CRD를 만들고, 이것을 등록한 것이다.
여기에는 maxReplicas란 정보가 들어있고, 이것을 CEL 표현식에서 활용했다.
꼭 CRD를 이용해야 하는 건 아니고 단순한 ConfigMap을 이용해도 상관 없다.

    - expression: "!has(params.optionalNumber) || (params.optionalNumber >= 5 && params.optionalNumber <= 10)"
	  message: "params is invaild"

정책을 쓸 때 params가 정말 제대로 된 것인지, 정책 단에서 체크를 해주는 것이 아무래도 안전할 것이다.
그래서 이런 식으로 미리 안전하게 추가 조건을 넣어주는 것이 좋다.

보통 다른 오브젝트를 오브젝트에 명시할 때는 흔히 Ref라는 접미사를 쓰는 것을 많이 보았을 것이다.
근데 왜 여기는 Kind 접미사가 붙느냐?
하는 당신 역시 칭찬합니다
정책 오브젝트에서는 사용할 파라미터가 어떤 식의 스키마를 가지고 있는지만 명시하여 사용하기 때문이다.
그래서 문서에서는 이걸 고정된(concrete) 형태의 정책 틀을 제공한다고도 표현한다.
아무튼 실제로 이렇게 paramKind를 이용하면 아래의 [[#ValidatingAdmissionPolicyBinding]]에서도 같이 단서를 제공해줘야 한다.

matchConditions

 matchConditions:
    - name: 'exclude-leases' # 이름은 고유하게
      expression: '!(request.resource.group == "coordination.k8s.io" && request.resource.resource == "leases")' 
    - name: 'exclude-kubelet-requests'
      expression: '!("system:nodes" in request.userInfo.groups)'
    - name: 'rbac' 
      expression: 'request.resource.group != "rbac.authorization.k8s.io"'

위에서 어떤 리소스를 검증하고 싶다!라는 것만으로 부족할 때 사용할 수 있는 필드이다.
정말 열심히 검증 대상을 필터링하고 싶으신 분이라면.. 이걸 쓰자.

보다시피 또 CEL 표현식을 이용해서 검증 대상이 될 놈들을 필터링할 수 있다..
위의 예시를 해석하자면..

이건 진짜 필요한가?

그냥 validations 필드에서 잘 지정해도 되는 건 아닐까..? 싶긴 하다.
다만 따지자면, 그런 게 있긴 하다.
a and b라는 조건문은 not a or not b와 논리적으로 상반되는데, 딱 봐도 논리적 상반을 나타내기 위해 들이는 수고가 커진다.
(괄호를 써도 되지만 그냥 예시를 위해 넘어간다.)
validations 필드는 거짓 조건을 판별하기 위한 CEL 조건문이 적히기에, 참으로 넘겨도 되는 조건문을 적을 때 상당히 귀찮아질 여지가 있다.
그래서 미리 참으로 넘겨도 될 놈들을 앞서서 필터링한다고 보면 될 것 같다.

  • matchConditions에서 거짓인 놈들은 검증에 구애되지 않으니 통과된다.
  • validaitions에서 거짓인 놈들은 검증 상 거짓 결정이 내려지니 통과되지 않을 수 있게 된다.

auditAnnotations

 auditAnnotations:
    - key: "high-replica-count"
      valueExpression: "'Deployment spec.replicas set to ' + string(object.spec.replicas)"

쿠버네티스 감사가 이뤄질 때, 추가 메시지를 넣어줄 수 있다.

"annotations": {
        "demo-policy.example.com/high-replica-count": "Deployment spec.replicas set to 128"
        # other annotations
    }

위의 예시는 실제 감사에서 이렇게 표현된다.

variables

spec:
  variables:
    - name: foo
      expression: "'foo' in object.spec.metadata.labels ? object.spec.metadata.labels['foo'] : 'default'"
  validations:
    - expression: variables.foo == 'bar'

각 검증 규칙에서 여러 번 사용하고 싶은 계산식이 있으면, 이를 또 한꺼번에 모아두는 게 도움이 될 것이다.
이를 위한 변수 필드가 또 있다.
이게 중요한 것이, 결국 표현식은 api 서버의 컴퓨팅 연산을 사용한다.
그래서 컴퓨팅 자원을 가급적 적게 쓰게 만드는 게 좋은데 이걸 활용하면 불필요한 중복 연산을 줄일 수 있을 것이다.

이 값은 정책이 적용될 때 바로 계산되는 건 아니고, 첫번째로 사용하는 검증 규칙이 있을 때 계산된다.

  variables:
  - name: environment
    expression: "'environment' in namespaceObject.metadata.labels ? namespaceObject.metadata.labels['environment'] : 'prod'"
  - name: exempt
    expression: "'exempt' in object.metadata.labels && object.metadata.labels['exempt'] == 'true'"
  - name: containers
    expression: "object.spec.template.spec.containers"
  - name: containersToCheck
    expression: "variables.containers.filter(c, c.image.contains('example.com/'))"
  validations:
  - expression: "variables.exempt || variables.containersToCheck.all(c, c.image.startsWith(variables.environment + '.'))"
    messageExpression: "'only ' + variables.environment + ' images are allowed in namespace ' + namespaceObject.metadata.name"

이런 식으로 활용 예시가 있다.

kubectl create deploy --image=dev.example.com/nginx invalid

이런 명령을 넣으면 이미지가 default 네임스페이스에서 dev로 시작하는 이미지 사용한다고 화낼 것이다.

ValidatingAdmissionPolicyBinding

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: "demo-binding-test.example.com"
spec:
  policyName: "demo-policy.example.com"
  validationActions: [Deny]
  matchResources:
    namespaceSelector:
      matchLabels:
        environment: test

정책을 실제로 적용할 때는 바인딩 오브젝트를 이용한다.
정책 이름을 적고, 검증에 대한 행동을 정의힌다.
그리고 어떤 리소스들이 정책에 해당하게 될지 범위를 지정해준다.
정책에서는 어떤 api 리소스가 적용될지를 지정했다면, 여기에서는 조금 더 클러스터 차원에서 범위를 정하는 느낌이다.

matchResources

어떤 요청이 정책으로 검증될지를 지정하는 필드이다.
정책의 [[#matchConstraints]]와 비슷하게, 여기에도 resourceRules를 명시해서 지정하는 것도 가능하다.[1]
그래도 이쪽은 보다시피 namespaceSelector를 사용할 수도 있어서, 조금 더 클러스터 관리적으로 지정할 수 있다는 점이 장점이랄까.
그래도 실상 같은 방식으로 이중 필터링을 하는 것이나 다름 없긴 하다고 생각한다..

여기에서는 objectSelector 필드를 사용할 수도 있는데, 이건 라벨 셀렉터와 같다.
이때의 라벨은 적용되기 이전, 적용되기 이후 전부 매칭시켜버리니, 조금 광범위하게 매칭하는 거라 보면 되겠다.

validationActions

정책에서 Fail이라고 결정이 났을 때, 해당 요청을 어떻게 처리할지에 대해 행동을 정의하는 필드이다.

리스트로 작성된 것을 보면 알 수 있듯이 여러 개를 한꺼번에 써줄 수 있다.
다만 Deny와 Warn은 Deny가 Warn을 품고 있기에 의미 없다.

paramRef

  paramRef:
    name: "replica-limit-test.example.com"
    namespace: "default"

정책에서 [[#paramKind]]를 쓴다면, 바인딩에서 이렇게 실제 리소스가 무엇인지 명시를 해줘야 한다.
즉, 여기에서 사용할 실제 리소스를 명시하게 되는 것이고, 정책 부분에서는 사용될 리소스의 스키마만 명시했다는 것을 알 수 있다.

paramKind쪽 예시에서 params != null이라는 조건을 추가해준 것도 바로 이것 때문이다.
실제 정책은 잘 만들었는데, 바인딩을 할 때 실수로 적절한 paramRef를 빼먹을 수도 있다.
그런 경우를 제어하기 위해 위에서 저런 조건을 추가해주는 게 좋다.

실제 사용할 파라미터가 네임스페이스 종속적이더라도, 굳이 네임스페이스를 명시해야만 하는 것은 아니다.
만약 해당 바인딩이 test 네임스페이스를 대상으로 한다면, 알아서 그 네임스페이스에 있는 param 오브젝트를 참고하게 된다!
[[#예시]]에서는 다른 이름으로 오브젝트를 만들었지만, 그냥 다른 네임스페이스로 구분을 둔 채 같은 이름으로 오브젝트를 만들어도 알아서 이것을 인식해서 적용해준다는 말이다.
이런 방식은 각 설정을 조금 더 유연하게 해준다는 장점이 있다.

name으로 파라미터를 매칭시켜도 되지만, selector 필드를 이용해서 라벨 셀렉터를 이용하는 방법도 있다!
(진짜 일관성좀 챙겨줬으면)
이렇게 하면 여러 개의 파라미터를 실제 정책에 넘기는 것도 가능해지는데, 이 경우 정책이 평가될 때 파라미터들을 각각 이용해서 정책이 수행된다.
그리고 이 검증들을 AND 조건을 평가해서 결과를 낼 것이다.

예시

조금 더 긴밀한 이해를 위해 예시를 넣자면, 이런 식이다.

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: "replicalimit-policy.example.com"
spec:
  failurePolicy: Fail
  paramKind:
    apiVersion: rules.example.com/v1
    kind: ReplicaLimit
  matchConstraints:
    resourceRules:
    - apiGroups:   ["apps"]
      apiVersions: ["v1"]
      operations:  ["CREATE", "UPDATE"]
      resources:   ["deployments"]
  validations:
    - expression: "object.spec.replicas <= params.maxReplicas"
      reason: Invalid

일단 이런 형태의 정책을 만들었다.

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: "replicalimit-binding-test.example.com"
spec:
  policyName: "replicalimit-policy.example.com"
  validationActions: [Deny]
  paramRef:
    name: "replica-limit-test.example.com"
    namespace: "default"
  matchResources:
    namespaceSelector:
      matchLabels:
        environment: test
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: "replicalimit-binding-nontest"
spec:
  policyName: "replicalimit-policy.example.com"
  validationActions: [Deny]
  paramRef:
    name: "replica-limit-prod.example.com"
    namespace: "default"
  matchResources:
    namespaceSelector:
      matchExpressions:
      - key: environment
        operator: NotIn
        values:
        - test

정책은 하나이지만, 이걸 다양한 바인딩을 할 수 있는데, 서로 다른 네임스페이스에 대해서 적용을 하는 것이 보인다.
그리고 이 둘은 각각 다른 paramRef를 가지고 있는 게 보인다.
같은 정책인 데도 각 네임스페이스에 대해 다른 정도의 기준을 적용할 수 있다는 말이다!

apiVersion: rules.example.com/v1
kind: ReplicaLimit
metadata:
  name: "replica-limit-test.example.com"
maxReplicas: 3
---
apiVersion: rules.example.com/v1
kind: ReplicaLimit
metadata:
  name: "replica-limit-prod.example.com"
maxReplicas: 100

이런 식으로 설정하면, 테스트 네임스페이스에서는 레플리카가 3개까지만 가능할 것이다.
반면 테스트가 아닌 네임스페이스에서는 레플리카를 100까지도 둘 수 있게 된다.

디버깅

...
  validations:
  - expression: "object.replicas > 1" # "object.spec.replicas > 1"라고 써야함
    message: "must be replicated"
    reason: Invalid

이런 식으로 관리자가 잘못 규칙을 쓰는 케이스가 있을 수 있다.
object 아래에 replicas가 없다면, 타입 에러가 발생한다.

status:
  typeChecking:
    expressionWarnings:
    - fieldRef: spec.validations[0].expression
      warning: |-
        apps/v1, Kind=Deployment: ERROR: <input>:1:7: undefined field 'replicas'
         | object.replicas > 1
         | ......^        

다행히도 이 경우 정책 오브젝트의 status 필드에 해당 정보가 출력되니 쉽게 디버깅할 수 있다!

근데 유의사항이 있다.

관련 문서

이름 noteType created
Admission Control knowledge 2025-01-20
Admission Webhook knowledge 2025-01-20
Validation Admission Policy knowledge 2025-03-17
Kyverno knowledge 2025-03-17
6W - api 구조와 보안 1 - 인증 published 2025-03-15
6W - api 보안 2 - 인가, 어드미션 제어 published 2025-03-16
E-Kyverno 기본 실습 topic/explain 2025-03-17
E-검증 승인 정책 실습 topic/explain 2025-03-17
S-exec 명령어가 승인 제어에 걸리는 이유 topic/shooting 2025-03-17

참고


  1. https://kubernetes.io/docs/reference/kubernetes-api/policy-resources/validating-admission-policy-binding-v1/ ↩︎